第二十六課:收尾與創建order訂單完成訂房手續 part2
昨天處理好了handleCheckBox讓我們可以勾選來更新我們的RoomNumber的state狀態,現在只要state中的RoomNumber,在我們按出送出按鈕前更新至我們的訂單後,以訂單的方式送出,同時我們送出時,也要幫room資料庫中的unavailableDates新增我們的訂房時間,防止送出後,重複預訂時間,實務面上如果有人訂房後沒付款,我可以再取消訂單跟刪除unavailableDates中的時間讓其他人可以訂房。
在完成送出訂單前,我們可以先設置一些保護措施,以免送出沒有勾選房型編號的資料,要確保資料都齊全才能建立訂單,
<button className='reservationbtn' disabled={roomNumber.length == 0} onClick={handleClick}> 現在預訂</button>
但這邊如果這樣做,你會發現訂單會失敗,會在你上傳一次資料才會是正確的,但明明用了await為何無效。這邊就要探討useState得re-render問題與如何用useEffect來讓axios.post可以真正上傳成功資料
先討論上面那個做法的錯誤。
所以想法是當我們想要在handleClick的函數內,一次執行說我要先更新我們orderData的state資料,再使用axios.post創建order訂單,這個前後順序不能換,就不能只使用async+await,因為useState的執行會比function執行來得慢,所以我們可以利用到useEffect最後render的特性(比useState還慢)來將我們的axios.post創建order訂單寫在useEffect內,同時只要當我們的設置dependency來規定說只有預訂後才會觸發useEffect,state有改變就會送回資料庫建立order資料,且這邊改變創建訂單state的情況只有在我們按下預訂按扭啟動更改setCreateOrderState的state資料時才會發生,所以不用怕其他的錯誤產生,這邊如果不要那麼複雜,完全是可以再多一步再做一個確認訂單後再送出,但這邊也可以當作熟悉useState與useEffect等各種render的先後順序會需要解決的問題。
useCreateOrder.js
import React, { useEffect, useState } from 'react'
import axios from "axios"
const useCreateOrder = (url,orderData,createOrderState) => {
const [Order, setOrder]=useState([]);
useEffect(() => {
const createOrder = async () => {
try {
const res = await axios.post(url, orderData)
console.log("useEffect 啟動")
} catch (error) {
console.log("上傳失敗")
}
}
if(createOrderState==true){
//設立確定送出實在使用這個createOrdered 才不會重複送出訂單
createOrder()
}
}, [createOrderState])
return{Order}
}
export default useCreateOrder
Reservation的handleClick
const [createOrderState, setCreateOrderState] = useState(false)
const {order} = useCreateOrder("/order",orderData,createOrderState)
//order就可以是創建成功的回傳的訂單資訊
const handleClick = async () => {
try {//這邊要塞兩個 一個創建訂單 一個更新unavavilableDates
await setOrderData((item) => ({ ...item, RoomNumberId: roomNumber }))
setCreateOrderState(true)//更改值,讓useCreateOrder可以被啟動
} catch (err) {
console.log("訂單或是住宿日期上傳失敗")
}
}
並送出訂單後會發現,這邊還有一個totalPrice,並沒有一起更新進去,這部分就給大家來自己練習看看了,只需要判讀 {room.price * DatesLength}再乘上該房型的總數等等的加總,最後一起送出,但這邊要判斷的話,必須先在checked那邊一起加總,或是如果覺得麻煩,總價也可以不要上傳,在後台時,再重新計算也可以。
這邊先完成第一步後,我們要來為我們的roomNumber中的unavailableDates新增我們的訂房時間,就像上面說的,新增訂單是為了給飯店管理員看,這邊新增unavailableDates是讓我們的訂房app這邊已經被訂走了,這段時間內不能在有人預訂。
所以首先我們要來先處理要登記的日期,一開始我們是紀錄startDate與endDate這兩個時間,但如果真的要紀錄資料庫的話,我們需要紀錄開始到結束的整段時間,比如說:開始時間是9/1、結束時間是9/15號,我們需要1~15號的時間都紀錄,這樣就能阻止有人預訂如13號這個中間時間。
這邊要在特別提到 new Date與getTime的宣告法
datesCalculate的while迴圈原理是這樣,我們先計入開始的日期,並慢慢把他的日期收錄至我們要回傳的datesList,然後收錄進Array後,我們會把我們的recordDates的天數加一,等到他的日期等於結束的日期時,就代表收錄完成,而我們這邊都日期換成毫秒方便紀錄,並值得注意的是startDate本身就是Date物件,但 new Date(startDate)必須要在宣告一次的原因是因為,如果不這樣做,你會發現startDate最後會被recordDates.getDate() + 1這行影響,因為日期是共享的,所以等於是迴圈結束後,我們的startDate已經變成了endDate的後一天的bug出現,這邊就是需要注意的全域變數問題。
//會影響到全域變數
export const ReservationDatesList = (startDate, endDate) => {
const recordDates = new Date(startDate);
//必須多這new Date()在宣告一次,不這樣會影響到startDate全域變數的值
const stopRecord = new Date(endDate);
const datesList = [];
while (recordDates <= stopRecord) {
datesList.push(recordDates.getTime());
recordDates.setDate(recordDates.getDate() + 1);
}
return { datesList };
}
成功後,可以在reservation內就使用上述函數來將我們有幾個晚上的日期都全部紀錄。
const { datesList } = ReservationDatesList(date[0]?.startDate, date[0]?.endDate)
然後接下來將他上傳製我們的roomNumber內的unavailableDates之中,關於這個更新方式,所以一樣我們先快速的製作updatedRoomDates的相關Api這邊,這邊將類似我們原本所製作的updatedRoom Api
//一樣是更新但我們只更新上傳unavailableDates的資料
router.put("/reservartiondates/:id",verifyUser,updatedRoomDates)
這邊開始寫函數前,我們可以在複習一下$push
與$pull
與$set
的mongoDB函數。
上面是之前講到的用法,我們將id使用$push
新增在別的資料庫,進而達到資料庫連動,而$set
比較像是項目對項目的去修改。所以我們這次是要將日期Array新增進去roomNumber中的unavailableDates之中,雖然不是資料庫與資料庫連接的使用,就是單純使用$push來將資料庫的資料新增項目進去,這邊與Api的put與post很適合都去反思,各自代表的意義,
export const updatedRoomDates = async (req, res, next) => {
const roomNumberId = req.params.id;
const dates = req.body.dates;
//到時候dates就會是我們傳入的datesList
try{
const updatedRoomDates = await Room.updateOne(
{ "roomNumbers._id": roomNumberId },
{
$push: {
"roomNumbers.$.unavailableDates": dates
},
}
);
res.status(200).json(updatedRoomDates)
} catch (error) {
next(errorMessage(500,"預訂日期更新失敗,可能為格式錯誤或是找不到其ID",error))
}
}
在insomnia上應該也可以很快速的測試輸入並成功新增時間區間
{
"dates":["1664640000000", "1664726400000", "1664812800000", "1664899200000", "1664985600000", "1665072000000", "1665158400000", "1665244800000"]
}
快速完成後,我們要來回到reservation內,在handldClick函數內一次完成我們的訂房時間上。
const updatedReservationDates = async () => {
try {
await Promise.all(
roomNumber.map((roomNumberId) => {
const res = axios.put(`/rooms/reservartiondates/${roomNumberId}`, {
dates: datesList,
});
})
);
} catch (error) {
console.log("上傳日期失敗")
}
};
然後再handleClick內,啟用updatedReservationDates,這樣就完成了我們想要的一次紀錄訂單內容並上傳訂單內容到資料庫。
updatedReservationDates()
測試後應該也會是成功的,並要將他訂房後讓其他人在同時段不能在再訂房,所以我們也要設置條件子句,讓checkBox的disabled能夠判讀。
const isNotAvailableDate = (roomNumber) => {
const isitNotAvailable =
roomNumber.unavailableDates.some((dates) => datesList.includes(new Date(dates).getTime()))
return isitNotAvailable
}
讓checkbox可以用這個函數去判讀同時段有沒有人預訂這個時間
<input type="checkbox" value={item._id} onChange={handleCheckBox} disabled={isNotAvailableDate(item)} />
並最後的最後可以加上完成的訂單畫面,與跳轉去首頁,就可以正式完成一次下單成功與紀錄日期等函數運作。
如下使用setTimeout延後執行函數特性來加入我們訂單成功訂購的特效。
這邊恭喜終於完成,基本上現在這個網站沒有後台也是可以運作,只要直接操作Api管理即可,並如果想要讓他直接上架讓大家測試使用,可以推薦國外評價很高的免費架站網站,讓你免費把你的react上架到網路上
https://www.netlify.com/
但這邊可能只是上架了一個React Ui介面,就跟我們的client Folder一樣我們啟用了,但Api folder如果沒有啟用是無法抓取mongoDB的資料的,所以這邊推薦即將快失效,但架設上超快超方便的heroku來執行我們的Api,等到他不能提供免費服務後,可以再找找有沒有相關的資源可以來讓我們存放Api,但基本上就算是付費使用,一個月也頂多幾百塊,對於一個能產生現金流的動態網站實在是綽綽有餘了,如想要再更進一步串接現金流,雖說我們無法直接對接綠界科技或是蘭新等第三方支付,但我們可以試著與linePay做結合也是不錯的方式,或是就單純轉帳,就可以完全不會有第三方抽成,又自己有個平台可以管理訂單,且把房間換成產品活生生就是一個電商平台。甚至還可以知道與了解預購的時間想要什麼時候之類的應用!
最後完成的動態網站連結(前台+Api完整版)
這邊Demo的結尾版有製作RWD,作為練習大家也可以自己做做看RWD的網頁讓手機也可以瀏覽基本的頁面,並到這邊總算是對整個動態網站告一段落,但如果資料要管理還是要使用到insomnia或是postman就太不方便了,所以明天開始會稍微帶到typeScript與tailwind,但因為方便關係,後台製作會以還是使用js來做後台,並做完後就是一個完整的電商前後台頁面,並希望能在剩下的四天盡量把功能完成,也為這次競賽最一個完美的ending。